#include <Kharlia/Engine/Common.h>
#include <Kharlia/Engine/Client.h>
#include <Kharlia/Engine/Engine.h>
#include <Kharlia/Engine/Units.h>
#include <Kharlia/Engine/Drawable.h>
#include <Kharlia/Engine/RenderView.h>
#include <Kharlia/Engine/TileData.h>
#include <Kharlia/Engine/Level.h>
#include <Kharlia/Engine/Actor.h>

#include <RendererModules/Direct3D10/CEGUIDirect3D10Texture.h>
#include <CEGUIExceptions.h>

#include <cmath>

#define TILE_W 16
#define TILE_H 16
#define TILE_EX 2

#define TILE_W_F 16.0f
#define TILE_H_F 16.0f
#define TILE_EX_F 2.0f

#define TILE_VERTEX_COUNT 6
//#define TILE_VERTEX_SIZE 2
//#define TILE_TEX_COORD_COUNT 4
//#define TILE_TEX_COORD_SIZE 2

#define CLAMP_RENDER_POSITION 0

#define DEBUG_GROUP_COLORING 0

#define HRDX(hr_) do { if (FAILED(hr_)) throw RuntimeError("DirectX call failed"); } while (0)

namespace kharlia { namespace engine {

/// Contains cached rendering information for a tile layer.
/// This is attached to the tile layer for future use.
struct FTileLayerRenderInfo: public FRenderInfo {
    ID3D10Buffer* Buffer;
    uint BufferSize;
    uint BufferOffset;

    // TODO: Have this store a weak reference to the client
    virtual ~FTileLayerRenderInfo() {
        if (Buffer)
            Buffer->Release();
    }
};

struct FTilesetRenderInfo: public FRenderInfo {
    //shared_ptr<FTexture> Texture;
    //bool LoggedFailure;

    //FTilesetRenderInfo(): LoggedFailure(false) {}
};

struct FEditorRenderInfo: public FRenderInfo { 
    //GLfloat* GridVerticies;
    //GLfloat SelectionVerticies[8];

    //FEditorRenderInfo(): GridVerticies(nullptr) {}
};

void ClampRenderPosition(FVector3F& pos) {
#if CLAMP_RENDER_POSITION
    static const float pxu = 1.0f / 16.0f;
    float fx = floorf(pos.X);
    float fy = floorf(pos.Y);
    pos.X = fx + floorf((pos.X - fx + (pxu / 2.0f)) * 16.0f) * pxu;
    pos.Y = fy + floorf((pos.Y - fy + (pxu / 2.0f)) * 16.0f) * pxu;
#endif
}

void ClampRenderPosition(FVector2F& pos) {
#if CLAMP_RENDER_POSITION
    static const float pxu = 1.0f / 16.0f;
    float fx = floorf(pos.X);
    float fy = floorf(pos.Y);
    pos.X = fx + floorf((pos.X - fx + (pxu / 2.0f)) * 16.0f) * pxu;
    pos.Y = fy + floorf((pos.Y - fy + (pxu / 2.0f)) * 16.0f) * pxu;
#endif
}

void CClient::DrawScene(TPtr<ACamera> camera, FRectF const& vp) {
    assert(mRendering);
    //if (!mTilesetTexture)
    //    return;

    // Retrieve level to be rendered form the camera
    TPtr<CLevel> const& level = camera->GetActor()->GetLevel();
    if (!level)
        throw RuntimeError("camera is not in a level");

    // Determine actual camera position
    FVector3F cameraPos;
    if (camera->GetTarget()) {
        if (camera->GetTarget()->GetActor()->GetLevel() != level)
            throw RuntimeError("A camera must be in the same level as its target");
        cameraPos = camera->GetTarget()->GetPosition();
    }
    else {
        TPtr<ATransform> pt = camera->GetActor()->GetTransform();
        if (pt)
            cameraPos = pt->GetPosition();
        else
            return;
    }

    // HACK: Clamping to the nearest pixel avoids a strange graphical error
    //       with tile texture coordinates.
    ClampRenderPosition(cameraPos);

    FSizeF levelSize = level->GetSize();
    FSizeF groupSize = level->GetTileGroupSize();

    // left, top, right, and bottom visible level coordinates (in tiles)
    float vcl = cameraPos.X - ((vp.Size.Width / 2.0f) / TILE_W_F);
    float vct = cameraPos.Y - ((vp.Size.Height / 2.0f) / TILE_H_F);

    float vcr = vcl + (vp.Size.Width / TILE_W_F);
    float vcb = vct + (vp.Size.Height / TILE_H_F);
        
    // top left and bottom right tilegroup positions on the level
    float tlpx = floorf(vcl / groupSize.Width);
    float tlpy = floorf(vct / groupSize.Height);
    float brpx = floorf(vcr / groupSize.Width);
    float brpy = floorf(vcb / groupSize.Height);

    // number of visible tilegroups
    int vsw = int(brpx - tlpx) + 1;
    int vsh = int(brpy - tlpy) + 1;

    // Configure DirectX
    SetDXViewport(mViewportSize);

    // Use an orthographic projection of tile coordinates
    mProjectionMatrixStack->LoadIdentity();
    D3DXMatrixOrthoOffCenterLH(mProjectionMatrixStack->GetTop(),
                               vcl, vcr, vcb, vct, 1.0f, 0.0f);

    // CEGUI has its own input layout, so we need to reset ours
    mDevice->IASetInputLayout(mVertexLayout);

    bool colSwitch;
    FVector2I tgp;
    D3DXCOLOR color;

    // Draw the tile layers for layer 0
    std::vector<shared_ptr<FLevelLayer>> const& layers = level->GetLayers();
    for (uint i = 0; i < layers.size(); i++) {
        FLevelLayer* layer = layers[i].get();
        if (!layer->Visible)
            continue;
            
        colSwitch = false;
        color.r = float(layer->Red) / 255.0f;
        color.g = float(layer->Green) / 255.0f;
        color.b = float(layer->Blue) / 255.0f;
        color.a = float(layer->Alpha) / 255.0f;

        shared_ptr<FTileset> const& tileset = layer->Tileset;
        ui::Texture* tilesetTexture = nullptr;
        //uint16 tilesetOffset = 0;
        if (tileset && tileset->GetTexture()) {
            tilesetTexture = tileset->GetTexture()->GetIsLoaded() ?
                                tileset->GetTexture()->GetIntTexture() : nullptr;
            //tilesetOffset = tileset->GetOffset();
        }
        if (tilesetTexture) {
            for (int y = 0; y < vsh; y++) {
                for (int x = 0; x < vsw; x++) {
                    // position of tilegroup on the level in tilegroups
                    //PointI spos(tlp + PointI(x, y));
                    float spx = tlpx + float(x);
                    float spy = tlpy + float(y);
                    if (spx < 0.0f || spx >= levelSize.Width ||
                        spy < 0.0f || spy >= levelSize.Height)
                    {
                        continue;
                    }
                    tgp.X = int(spx);
                    tgp.Y = int(spy);
                    shared_ptr<FTileGroup> const& group = level->GetTileGroup(tgp);
                    shared_ptr<FTileLayer> const& tileLayer = group->GetLayer(i);
                    // position of the tilegroup on the level in tiles (real position)
                    float srx = spx * groupSize.Width;
                    float sry = spy * groupSize.Height;

                    mProjectionMatrixStack->Push();
                    mProjectionMatrixStack->TranslateLocal(srx, sry, 0.0f);

                    if (DEBUG_GROUP_COLORING) {
                        // level color highlighting for debugging
                        color = colSwitch ? D3DXCOLOR(1.0f, 0.5f, 0.5f, 1.0f) :
                                            D3DXCOLOR(0.5f, 0.5f, 1.0f, 1.0f);
                        colSwitch = !colSwitch;
                    }

                    DrawTileLayer(tileLayer.get(), tilesetTexture, color);

                    mProjectionMatrixStack->Pop();
                }
                if (DEBUG_GROUP_COLORING)
                    colSwitch = !colSwitch;
            }
        }
        // Draw the actors for layer 0
        DrawLevelActors(level.GetPtr(), 0, vcl, vct, vcr, vcb);
    }
    // Draw the editor brush
    //TPtr<CEngine> eng = GetEngine();
    //if (eng->GetEditorEnabled()) {
    //    if (!mEditorRenderInfo)
    //        mEditorRenderInfo.reset(new FEditorRenderInfo());
    //    //if (eng->GetEditorBrushVisible())
    //    //    DrawEditorBrush(*tilesetTexture);
    //    if (eng->GetEditorSelectionVisible())
    //        DrawEditorSelection();
    //}
}

void CClient::DrawEditorBrush(ui::Texture* tex) {
    assert(mRendering);
    //TPtr<CEngine> eng = GetEngine();
    //if (!eng->GetEditorBrushVisible() || !eng->GetEditorBrush())
    //    return;
    //shared_ptr<FTileLayer> const& bl = eng->GetEditorBrushLayer();
    //assert(bl);
    //FVector2F pos = ConvertScreenToTileCoords(mMousePos);
    //pos.X = floorf(pos.X);
    //pos.Y = floorf(pos.Y);
    //eng->SetEditorBrushPos(pos);
}

void CClient::DrawEditorSelection() {
    assert(mRendering);

    //TPtr<CEngine> eng = GetEngine();
    //FRectF const& selection = eng->GetEditorSelection();
    //if (!eng->GetEditorSelectionVisible() ||
    //    selection.GetArea() < 1.0f)
    //    return;
    //FEditorRenderInfo* ri = static_cast<FEditorRenderInfo*>(mEditorRenderInfo.get());
}

// Draw the actors in a specified level on the specified layer with visible
// coordinates (bounds) arguments
void CClient::DrawLevelActors(
    CLevel* level,
    int layerindex,
    float clipl,
    float clipt,
    float clipr,
    float clipb
    )
{
    std::vector<TPtr<CActor>> const& actors = level->GetActors();
    for (uint i = 0; i < actors.size(); i++) {
        TPtr<CActor> const& actor = actors[i];
        TPtr<ARender> const& renderComp = actor->GetRender();
        if (!mShowAllActors && (!renderComp || renderComp->GetHidden()))
            continue;
        TPtr<ATransform> const& transform = actor->GetTransform();
        if (transform) {
            FVector3F const& pos = transform->GetPosition();
            if (pos.X < clipl || pos.X > clipr || pos.Y > clipb || pos.Y < clipt)
                continue;
        }
        DrawActor(actor.GetPtr());
    }
}

void CClient::DrawActor(CActor* actor) {
    // Using temporary texture for now
    assert(mRendering);

    TPtr<ARender> const& renderable = actor->GetRender();
    if (renderable) {
        renderable->Draw(this);
    }
    else if (mShowAllActors) {
        TPtr<ATransform> const& trans = actor->GetTransform();
        if (trans)
            DrawSprite(trans->GetPosition(),
                       TempActorTextureR,
                       FRectF(0.0, 0.0, 32.0, 32.0),
                       ui::colour(1.0, 1.0, 1.0, 1.0));
    }

    // Draw bounding boxes around actors
    if (mShowActorBounds) {
        TPtr<ATransform> const& trans = actor->GetTransform();
        TPtr<ACollider> const& col = actor->GetCollider();
        if (trans && col) {
            FRectF const& bounds = col->GetBounds();
            if (bounds.GetArea() > 0.0f) {
                FVector3F const& pos = trans->GetPosition();

                FVertex v[5];
                v[0].Pos.x = bounds.GetLeft();
                v[0].Pos.y = bounds.GetTop();
                v[0].Pos.z = 0.0f;
                v[1].Pos.x = bounds.GetRight();
                v[1].Pos.y = bounds.GetTop();
                v[1].Pos.z = 0.0f;
                v[2].Pos.x = bounds.GetRight();
                v[2].Pos.y = bounds.GetBottom();
                v[2].Pos.z = 0.0f;
                v[3].Pos.x = bounds.GetLeft();
                v[3].Pos.y = bounds.GetBottom();
                v[3].Pos.z = 0.0f;
                v[4].Pos.x = bounds.GetLeft();
                v[4].Pos.y = bounds.GetTop();
                v[4].Pos.z = 0.0f;

                DrawLine(v, 5, FVector2F(pos.X, pos.Y));
            }
        }
    }
}

void CClient::DrawLine(
    FVertex const* verts,
    uint count,
    FVector2F const& offset,
    ui::colour const& color
    )
{
    assert(mRendering);
    assert(verts && count > 0 && count <= VERTEX_BUFFER_SIZE);

    // Buffer the verticies
    FVertex* v = nullptr;
    HRDX(mVertexBuffer->Map(D3D10_MAP_WRITE_DISCARD, 0, (void**)&v));
    memcpy(v, verts, count * sizeof(FVertex));
    mVertexBuffer->Unmap();

    // Setup the input assembler and effect
    uint stride = sizeof(FVertex);
    uint boffset = 0;
    mDevice->IASetVertexBuffers(0, 1, &mVertexBuffer, &stride, &boffset);
    mDevice->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_LINESTRIP);
    mProjectionMatrixStack->Push();
    mProjectionMatrixStack->TranslateLocal(offset.X, offset.Y, 0.0f);
    mProjectionMatrixEV->SetMatrix(*mProjectionMatrixStack->GetTop());
    mProjectionMatrixStack->Pop();
    mGlobalColorEV->SetFloatVector(D3DXCOLOR(color.getRed(),
                                             color.getGreen(),
                                             color.getBlue(),
                                             color.getAlpha()));

    // Render the lines
    D3D10_TECHNIQUE_DESC td;
    mNoTexTechnique->GetDesc(&td);

    for (uint i = 0; i < td.Passes; i++) {
        mNoTexTechnique->GetPassByIndex(i)->Apply(0);
        mDevice->Draw(count, 0);
    }
}

void CClient::DrawSprite(
    FVector3F const& pos,
    shared_ptr<FTexture> const& tex,
    FRectF const& rect,
    ui::colour const& color
    )
{
    DrawSprite(pos, tex->GetIntTexture(), rect, color);  
}

void CClient::DrawSprite(
    FVector3F const& pos,
    ui::Texture const* texbase,
    FRectF const& rect,
    ui::colour const& color
) {
    if (!mRendering)
        throw RuntimeError("not currently rendering");
    if (!texbase)
        throw NullPointerError("DrawSprite(): texture");

    ui::Direct3D10Texture const* tex = static_cast<ui::Direct3D10Texture const*>(texbase);
    ui::Vector2 const& scaling = tex->getTexelScaling();

    // Buffer our sprite verticies
    FVertex* v = nullptr;
    mVertexBuffer->Map(D3D10_MAP_WRITE_DISCARD, 0, (void**)&v);

    v[0].Pos.x = pos.X;
    v[0].Pos.y = pos.Y;
    v[0].Pos.z = 0.0f;
    v[0].TexCoord.x = rect.Pos.X * scaling.d_x;
    v[0].TexCoord.y = rect.Pos.Y * scaling.d_y;

    v[1].Pos.x = pos.X + (rect.Size.Width / 16.0f);
    v[1].Pos.y = pos.Y;
    v[1].Pos.z = 0.0f;
    v[1].TexCoord.x = (rect.Pos.X + rect.Size.Width) * scaling.d_x;
    v[1].TexCoord.y = rect.Pos.Y * scaling.d_y;

    v[2].Pos.x = pos.X;
    v[2].Pos.y = pos.Y + (rect.Size.Height / 16.0f);
    v[2].Pos.z = 0.0f;
    v[2].TexCoord.x = rect.Pos.X * scaling.d_x;
    v[2].TexCoord.y = (rect.Pos.Y + rect.Size.Height) * scaling.d_y;

    v[3].Pos.x = pos.X + (rect.Size.Width / 16.0f);
    v[3].Pos.y = pos.Y + (rect.Size.Height / 16.0f);
    v[3].Pos.z = 0.0f;
    v[3].TexCoord.x = (rect.Pos.X + rect.Size.Width) * scaling.d_x;
    v[3].TexCoord.y = (rect.Pos.Y + rect.Size.Height) * scaling.d_y;

    mVertexBuffer->Unmap();

    // Setup the device and shader effect for rendering
    uint stride = sizeof(FVertex);
    uint offset = 0;
    mDevice->IASetVertexBuffers(0, 1, &mVertexBuffer, &stride, &offset);
    mDevice->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
    mTextureSR->SetResource(tex->getDirect3DShaderResourceView());
    mProjectionMatrixEV->SetMatrix(*mProjectionMatrixStack->GetTop());
    mGlobalColorEV->SetFloatVector(D3DXCOLOR(color.getRed(),
                                             color.getGreen(),
                                             color.getBlue(),
                                             color.getAlpha()));

    // Render the sprite
    D3D10_TECHNIQUE_DESC td;
    mFullTechnique->GetDesc(&td);

    for (uint i = 0; i < td.Passes; i++) {
        mFullTechnique->GetPassByIndex(i)->Apply(0);
        mDevice->Draw(4, 0);
    }
}

bool CClient::OnTilesetTextureNotFound(
    shared_ptr<FTileset> const& tileset,
    shared_ptr<FTexture>& outResource)
{
    return true;
}

bool CClient::OnTilesetTextureResourceInvalid(
    shared_ptr<FTileset> const& tileset,
    shared_ptr<FTexture>& outResource)
{
    return true;
}

ui::Texture* CClient::GetTilesetTexture(CLevel& level) {
    return nullptr;
}

void CClient::DrawTileLayer(FTileLayer* t, ui::Texture* texb, D3DXCOLOR& color) {
    if (t->GetRenderInfoInvalid()) {
        if (t->GetRenderInfo())
            delete t->GetRenderInfo();
        t->SetRenderInfo(nullptr);
        t->SetRenderInfoInvalid(false);
    }
    if (t->GetRenderInfo() == nullptr) {
        // Fill the drawable info by creating and buffering a tex coord array
        // and using an existing vertex array for the tiles
        FTileData const* td = t->GetTileData().get();

        // Tile size
        int tw = td->GetSize().Width;
        int th = td->GetSize().Height;

        // Tile count
        int tc = tw * th;

        // Tiles vector ref
        tile_t const* tls = &td->GetTiles().front();
            
        // X and y scale
        ui::Vector2 const& scaling = texb->getTexelScaling();
        float const& xsc = scaling.d_x;
        float const& ysc = scaling.d_y;
            
        // array size
        int    tcas = tc * TILE_VERTEX_COUNT;

        // vertex and tex coord arrays
        scoped_array<FVertex> tca_(new FVertex[tcas]);
        FVertex* tca = tca_.get();

        // tile code and coordinates
        tile_t tcd;
        float tcx;
        float tcy;
            
        // array index
        int ti = 0;
        for (int y = 0; y < th; y++) {
            for (int x = 0; x < tw; x++) {
                tcd = tls[(y * tw) + x];
                tcx = float(TileToX(tcd));
                tcy = float(TileToY(tcd));

                // T1 Top left
                tca[ti].Pos = D3DXVECTOR3(float(x), float(y), 0.0f);
                tca[ti].TexCoord = D3DXVECTOR2((tcx * 16.0f) * xsc, (tcy * 16.0f) * ysc);
                ti += 1;
                
                // T1 Top right
                tca[ti].Pos = D3DXVECTOR3(float(x) + 1.0f, float(y), 0.0f);
                tca[ti].TexCoord = D3DXVECTOR2(((tcx + 1.0f) * 16.0f) * xsc, (tcy * 16.0f) * ysc);
                ti += 1;

                // T1 Bottom Left
                tca[ti].Pos = D3DXVECTOR3(float(x), float(y) + 1.0f, 0.0f);
                tca[ti].TexCoord = D3DXVECTOR2((tcx * 16.0f) * xsc, ((tcy + 1.0f) * 16.0f) * ysc);
                ti += 1;

                // T2 Bottom Left
                tca[ti].Pos = D3DXVECTOR3(float(x), float(y) + 1.0f, 0.0f);
                tca[ti].TexCoord = D3DXVECTOR2((tcx * 16.0f) * xsc, ((tcy + 1.0f) * 16.0f) * ysc);
                ti += 1;

                // T2 Bottom right
                tca[ti].Pos = D3DXVECTOR3(float(x) + 1.0f, float(y) + 1.0f, 0.0f);
                tca[ti].TexCoord = D3DXVECTOR2(((tcx + 1.0f) * 16.0f) * xsc, ((tcy + 1.0f) * 16.0f) * ysc);
                ti += 1;
     
                // T1 Top right
                tca[ti].Pos = D3DXVECTOR3(float(x) + 1.0f, float(y), 0.0f);
                tca[ti].TexCoord = D3DXVECTOR2(((tcx + 1.0f) * 16.0f) * xsc, (tcy * 16.0f) * ysc);
                ti += 1;
            }
        }
        uint tcasb = sizeof(FVertex) * tcas;

        // Create buffer
        D3D10_SUBRESOURCE_DATA sd;
        sd.pSysMem = (void*)tca;
        sd.SysMemPitch = tcasb; // not needed, but for debugging
        sd.SysMemSlicePitch = 0;

        ID3D10Buffer* tvb;
        HRESULT hr;
        D3D10_BUFFER_DESC bd;
        bd.Usage = D3D10_USAGE_DEFAULT;
        bd.ByteWidth = tcasb;
        bd.BindFlags = D3D10_BIND_VERTEX_BUFFER;
        bd.CPUAccessFlags = 0;
        bd.MiscFlags = 0;
        hr = mDevice->CreateBuffer(&bd, &sd, &tvb);
        if (FAILED(hr))
            throw RuntimeError("failed to create vertex bfufer for tile layer");

        FTileLayerRenderInfo* info = new FTileLayerRenderInfo;
        info->Buffer = tvb; // transfer ownership
        info->BufferOffset = 0;
        info->BufferSize = tcas;

        t->SetRenderInfo(info);
    }
    FTileLayerRenderInfo* ri = static_cast<FTileLayerRenderInfo*>(t->GetRenderInfo());
    auto tex = static_cast<ui::Direct3D10Texture*>(texb);
    
    uint stride = sizeof(FVertex);
    uint boffset = 0;
    mDevice->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    mDevice->IASetVertexBuffers(0, 1, &ri->Buffer, &stride, &boffset);
    mTextureSR->SetResource(tex->getDirect3DShaderResourceView());
    mProjectionMatrixEV->SetMatrix(*mProjectionMatrixStack->GetTop());
    //D3DXCOLOR colordx(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
    mGlobalColorEV->SetFloatVector(color);

    D3D10_TECHNIQUE_DESC td;
    mFullTechnique->GetDesc(&td);

    for (uint i = 0; i < td.Passes; i++) {
        mFullTechnique->GetPassByIndex(i)->Apply(0);
        mDevice->Draw(ri->BufferSize, ri->BufferOffset);
    }
}

void CClient::ReleaseTileVertexBuffers() {
    for (auto it = mTileVertexBuffers.begin(); it != mTileVertexBuffers.end();) {
        if (it->second) {
            it->second->Release();
            mTileVertexBuffers.erase(it++);
        }
        else
            it++;
    }
}

void CClient::BuildTileset(shared_ptr<FTileset> tileset) {
}

}} //namespace Kharlia